package com.crm.miaohe.service;

import com.crm.miaohe.common.ApplicationConstant;
import com.crm.miaohe.controller.authentication.request.LoginRequest;
import com.crm.miaohe.controller.authentication.request.user.AuthUserQueryRequest;
import com.crm.miaohe.exception.ErpBusinessException;
import com.crm.miaohe.mapper.TblAuthUserMapper;
import com.crm.miaohe.mapper.TblCustomerMapper;
import com.crm.miaohe.model.TblAuthUser;
import com.crm.miaohe.model.TblAuthUserExample;
import com.crm.miaohe.model.TblCustomer;
import com.crm.miaohe.model.TblCustomerExample;
import com.crm.miaohe.security.Role;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Slf4j
@Service
public class AuthenticationServiceImpl implements AuthenticationService {

    private final DefaultTokenServices tokenServices;
    private final AuthenticationManager authenticationManager;
    private final TblAuthUserMapper authUserMapper;
    private final TblCustomerMapper customerMapper;
    private final PasswordEncoder passwordEncoder;

    public AuthenticationServiceImpl(DefaultTokenServices tokenServices,
                                     AuthenticationManager authenticationManager,
                                     TblAuthUserMapper authUserMapper,
                                     TblCustomerMapper customerMapper,
                                     PasswordEncoder passwordEncoder) {
        this.tokenServices = tokenServices;
        this.authenticationManager = authenticationManager;
        this.authUserMapper = authUserMapper;
        this.customerMapper = customerMapper;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    @Transactional(noRollbackFor = AuthenticationException.class)
    public OAuth2AccessToken authenticate(LoginRequest request, String platform) {
        // 判断登录平台
        UsernamePasswordAuthenticationToken usernamePasswordToken;
        switch (platform) {
            case ApplicationConstant.PLATFORM_CUSTOMER: { // Frontend
                usernamePasswordToken = authenticateCustomer(request.getUsername(), request.getPassword());
                break;
            }
            case ApplicationConstant.PLATFORM_MANAGEMENT: { // Backend
                usernamePasswordToken = authenticate(request.getUsername(), request.getPassword());
                break;
            }
            default:
                throw new ErpBusinessException(HttpStatus.BAD_REQUEST,
                        ApplicationConstant.PLATFORM_NOT_SUPPORTED, platform);
        }
        /*
         * 这一步只是将 Authentication 对象中的 authenticated 属性变为 true。
         * 因为上面已经认证过了，所以不需要再一次认证
         * 返回的 Authentication 对象用于下面创建 access_token
         */
        Authentication authentication = authenticationManager.authenticate(usernamePasswordToken);
        AuthorizationRequest authorizationRequest = new AuthorizationRequest(ApplicationConstant.CLIENT_ID, List.of("read", "write"));
        // 将 Approve 设置成 true 表示同意登录，以后使用生成好的 token 能正常访问 API
        authorizationRequest.setApproved(true);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), authentication);
        // 生成 access_token 和 refresh_token
        return tokenServices.createAccessToken(oAuth2Authentication);
    }

    @Override
    @Transactional(noRollbackFor = AuthenticationException.class)
    public OAuth2AccessToken authenticateEncode(LoginRequest request, String platform) {
        // 判断登录平台
        UsernamePasswordAuthenticationToken usernamePasswordToken;
        switch (platform) {
            case ApplicationConstant.PLATFORM_CUSTOMER: { // Frontend
                usernamePasswordToken = authenticateCustomerEncode(request.getUsername(), request.getPassword());
                break;
            }
            case ApplicationConstant.PLATFORM_MANAGEMENT: { // Backend
                usernamePasswordToken = authenticate(request.getUsername(), request.getPassword());
                break;
            }
            default:
                throw new ErpBusinessException(HttpStatus.BAD_REQUEST,
                        ApplicationConstant.PLATFORM_NOT_SUPPORTED, platform);
        }
        /*
         * 这一步只是将 Authentication 对象中的 authenticated 属性变为 true。
         * 因为上面已经认证过了，所以不需要再一次认证
         * 返回的 Authentication 对象用于下面创建 access_token
         */
        Authentication authentication = authenticationManager.authenticate(usernamePasswordToken);
        AuthorizationRequest authorizationRequest = new AuthorizationRequest(ApplicationConstant.CLIENT_ID, List.of("read", "write"));
        // 将 Approve 设置成 true 表示同意登录，以后使用生成好的 token 能正常访问 API
        authorizationRequest.setApproved(true);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), authentication);
        // 生成 access_token 和 refresh_token
        return tokenServices.createAccessToken(oAuth2Authentication);
    }

    @Override
    public OAuth2AccessToken refreshToken(String refreshToken) {
        Map<String, String> params = Map.of("grant_type", "refresh_token", "client_id", ApplicationConstant.CLIENT_ID, "refresh_token", refreshToken);
        return tokenServices.refreshAccessToken(refreshToken, new TokenRequest(params, ApplicationConstant.CLIENT_ID, List.of("read", "write"), "refresh_token"));
    }

    @Override
    public void logout(String accessToken) {
        tokenServices.revokeToken(accessToken);
    }

    @NotNull
    private UsernamePasswordAuthenticationToken authenticate(String username, String password) {
        TblAuthUserExample example = new TblAuthUserExample();
        example.createCriteria().andPhoneNumberEqualTo(username).andDeletedEqualTo(false);
        example.or().andUsernameEqualTo(username).andDeletedEqualTo(false);
        TblAuthUser authUser = authUserMapper.selectByExample(example)
                .stream().findAny().orElseThrow(() -> new UsernameNotFoundException(ApplicationConstant.USERNAME_NOT_FOUND));
        if (!passwordEncoder.matches(password, authUser.getPassword())) {
            //如果是管理员就不做登录限制
            if(!authUser.getRoleId().equals(Role.ADMIN.getId())){
                authUser.setLoginFailedCount((authUser.getLoginFailedCount() == null ? 0 : authUser.getLoginFailedCount()) + 1);
                if (authUser.getLoginFailedCount() > 5) {
                    authUser.setLoginLocked(true);
                    authUser.setLoginUnlockTime(LocalDateTime.now().plusHours(2));
                }
                authUserMapper.updateByPrimaryKeySelective(authUser);
            }
            throw new BadCredentialsException(ApplicationConstant.INVALID_USERNAME_PWD);
        }
        if (authUser.getLoginLocked() && authUser.getLoginUnlockTime().isBefore(LocalDateTime.now())) {
            log.warn("This account is prohibited from logging. account: {}", username);
            throw new LockedException(ApplicationConstant.LOGIN_LOCKED);
        }
        if (authUser.getAccountLocked()) {
            log.warn("This account is locked. account: {}", username);
            throw new LockedException(ApplicationConstant.ACCOUNT_LOCKED);
        }
        if (Objects.isNull(authUser.getRoleId())) {
            log.warn("This account is not assigned permissions. account: {}", username);
            throw new DisabledException(ApplicationConstant.ACCOUNT_ROLE_ABSENCE);
        }
        Role role = Role.fromId(authUser.getRoleId());
        if (Objects.isNull(role)) {
            log.warn("This account is assigned to an unknown role, please check database. role: {}", authUser.getRoleId());
            throw new DisabledException(ApplicationConstant.ROLE_NOT_SUPPORTED);
        }
        return new UsernamePasswordAuthenticationToken(username, password, List.of(role));
    }

    /**
     * 认证 Customer 用户
     *
     * @param phoneNumber 电话号码
     * @param password    密码
     * @return {@link UsernamePasswordAuthenticationToken}
     * @throws UsernameNotFoundException 电话号码未注册的情况
     * @throws BadCredentialsException   用户名密码不正确的情况
     * @throws DisabledException         用户状态未承认的情况
     */
    @NotNull
    private UsernamePasswordAuthenticationToken authenticateCustomer(String phoneNumber, String password) {
        // Customer只能使用电话号码登录
        TblCustomerExample example = new TblCustomerExample();
        example.createCriteria().andPhoneNumberEqualTo(phoneNumber).andDeletedEqualTo(false);
        TblCustomer customer = customerMapper.selectByExample(example).stream().findFirst()
                .orElseThrow(() -> {
                    log.error("This phone number is not registered. PhoneNumber: {}", phoneNumber);
                    return new UsernameNotFoundException(ApplicationConstant.USERNAME_NOT_FOUND);
                });
        if (customer.getLocked()) {
            throw new LockedException(ApplicationConstant.ACCOUNT_LOCKED);
        }
        if (!passwordEncoder.matches(password, customer.getPassword())) {
            log.error("Password is wrong. PhoneNumber: {}", phoneNumber);
            throw new BadCredentialsException(ApplicationConstant.INVALID_USERNAME_PWD);
        }
        if (customer.getApproved() == null || !customer.getApproved()) {
            log.error("The customer has not been approved yet. PhoneNumber: {}", phoneNumber);
            throw new DisabledException(ApplicationConstant.WAITING_APPROVE);
        }
        return new UsernamePasswordAuthenticationToken(phoneNumber, password, List.of(Role.CUSTOMER));
    }

    /**
     * 认证 Customer 用户
     *
     * @param phoneNumber 电话号码
     * @param password    密码
     * @return {@link UsernamePasswordAuthenticationToken}
     * @throws UsernameNotFoundException 电话号码未注册的情况
     * @throws BadCredentialsException   用户名密码不正确的情况
     * @throws DisabledException         用户状态未承认的情况
     */
    @NotNull
    private UsernamePasswordAuthenticationToken authenticateCustomerEncode(String phoneNumber, String password) {
        // Customer只能使用电话号码登录
        TblCustomerExample example = new TblCustomerExample();
        example.createCriteria().andPhoneNumberEqualTo(phoneNumber).andDeletedEqualTo(false);
        TblCustomer customer = customerMapper.selectByExample(example).stream().findFirst()
                .orElseThrow(() -> {
                    log.error("This phone number is not registered. PhoneNumber: {}", phoneNumber);
                    return new UsernameNotFoundException(ApplicationConstant.USERNAME_NOT_FOUND);
                });
        if (customer.getLocked()) {
            throw new LockedException(ApplicationConstant.ACCOUNT_LOCKED);
        }

        if (!customer.getPassword().equals(password)) {
            log.error("Password is wrong. PhoneNumber: {}", phoneNumber);
            throw new BadCredentialsException(ApplicationConstant.INVALID_USERNAME_PWD);
        }
        if (customer.getApproved() == null || !customer.getApproved()) {
            log.error("The customer has not been approved yet. PhoneNumber: {}", phoneNumber);
            throw new DisabledException(ApplicationConstant.WAITING_APPROVE);
        }
        return new UsernamePasswordAuthenticationToken(phoneNumber, password, List.of(Role.CUSTOMER));
    }

    @Override
    public boolean checkUserByUserNameOrPhoneNumber(AuthUserQueryRequest registerRequest) {

        TblAuthUserExample example = new TblAuthUserExample();
        if (!ObjectUtils.isEmpty(registerRequest.getUsername())) {
            example.createCriteria().andUsernameEqualTo(registerRequest.getUsername());
        }

        if (!ObjectUtils.isEmpty(registerRequest.getPhoneNumber())) {
            example.createCriteria().andPhoneNumberEqualTo(registerRequest.getPhoneNumber());
        }
        return authUserMapper.countByExample(example) > 0;
    }
}
